﻿using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.IO;

namespace EffiProz.Core.Lib.Tar
{
    public class TarGenerator
    {



        protected TarFileOutputStream archive;
        protected ArrayList entryQueue = new ArrayList();
        protected long paxThreshold = 0100000000000L;

        // in bytes.  Value here exactly = 8GB.

        /**
         * When data file is this size or greater, in bytes, a
         * Pix Interchange Format 'x' record will be created and used for the file
         * entry.
         * <P/>
         * <B>Limitation</B>
         * At this time, PAX is only implemented for entries added a Files,
         * not entries added as Stream.
         */
        public void setPaxThreshold(long paxThreshold)
        {
            this.paxThreshold = paxThreshold;
        }

        /**
         * @see #setPaxThreshold(long)
         */
        public long getPaxThreshold()
        {
            return paxThreshold;
        }

        /**
         * Compression is determined directly by the suffix of the file name in
         * the specified path.
         *
         * @param inFile  Absolute or relative (from user.dir) File for
         *                     tar file to be created.  getName() Suffix must
         *                     indicate tar file and may indicate a compression
         *                     method.
         * @param overWrite    True to replace an existing file of same path.
         * @param blocksPerRecord  Null will use default tar value.
         */
        public TarGenerator(FileInfo inFile, bool overWrite,
                            int? blocksPerRecord)
        {

            FileInfo archiveFile = inFile;

            // Do this so we can be sure .getParent*() is non-null.  (Also allows
            // us to use .getPath() instead of very long .getAbsolutePath() for
            // error messages.
            int compression = TarFileOutputStream.Compression.NO_COMPRESSION;

            if (archiveFile.Name.EndsWith(".tgz")
                    || archiveFile.Name.EndsWith(".tar.gz"))
            {
                compression = TarFileOutputStream.Compression.GZIP_COMPRESSION;
            }
            else if (archiveFile.Name.EndsWith(".tar"))
            {

                // purposefully do nothing
            }
            else
            {
                throw new ArgumentException(
                    RB.singleton.getString(
                        RB.UNSUPPORTED_EXT, this.GetType().FullName,
                        archiveFile.FullName));
            }

            if (archiveFile.Exists)
            {
                if (!overWrite)
                {
                    throw new IOException(RB.singleton.getString(RB.DEST_EXISTS,
                            archiveFile.FullName));
                }
            }
            else
            {
                DirectoryInfo parentDir = archiveFile.Directory;

                // parentDir will be absolute, since archiveFile is absolute.
                if (parentDir.Exists)
                {
                    //if (!parentDir.isDirectory()) {
                    //    throw new IOException(
                    //        RB.singleton.getString(
                    //            RB.PARENT_NOT_DIR, parentDir.getPath()));
                    //}

                    //if (!parentDir.canWrite()) {
                    //    throw new IOException(
                    //        RB.singleton.getString(
                    //            RB.CANT_WRITE_PARENT, parentDir.getPath()));
                    //}
                }
                else
                {
                    parentDir.Create();
                }
            }

            archive = (blocksPerRecord == null)
                      ? new TarFileOutputStream(archiveFile, compression)
                      : new TarFileOutputStream(archiveFile, compression,
                                                (int)blocksPerRecord);

            if (blocksPerRecord != null && TarFileOutputStream.debug)
            {
                Console.Out.WriteLine(RB.singleton.getString(RB.BPR_WRITE,
                        (int)blocksPerRecord));
            }
        }

        public void queueEntry(FileInfo file)
        {
            queueEntry(null, file);
        }

        public void queueEntry(String entryPath,
                               FileInfo file)
        {
            entryQueue.Add(new TarEntrySupplicant(entryPath, file, archive));
        }

        /**
         * This method does not support Pax Interchange Format, nor data sizes
         * greater than 2G.
         * <P/>
         * This limitation may or may not be eliminated in the future.
         */
        public void queueEntry(String entryPath, Stream inStream,
                               int maxBytes)
        {
            entryQueue.Add(new TarEntrySupplicant(entryPath, inStream, maxBytes,
                                                  '0', archive));
        }

        /**
         * This method does release all of the streams, even if there is a failure.
         */
        public void write()
        {

            if (TarFileOutputStream.debug)
            {
                Console.Out.WriteLine(RB.singleton.getString(RB.WRITE_QUEUE_REPORT,
                        entryQueue.Count));
            }

            TarEntrySupplicant entry;

            try
            {
                for (int i = 0; i < entryQueue.Count; i++)
                {
                    Console.Error.Write((i + 1) + " / "
                                     + entryQueue.Count + ' ');

                    entry = (TarEntrySupplicant)entryQueue[i];

                    Console.Error.Write(entry.getPath() + "... ");

                    if (entry.getDataSize() >= paxThreshold)
                    {
                        entry.makeXentry().write();
                        Console.Error.Write("x... ");
                    }

                    entry.write();
                    archive.assertAtBlockBoundary();
                    Console.Error.WriteLine();
                }

                archive.finish();
            }
            catch (IOException ioe)
            {
                Console.Error.WriteLine();    // Exception should cause a report

                try
                {

                    // Just release resources from any Entry's input, which may be
                    // left open.
                    for (int i = 0; i < entryQueue.Count; i++)
                    {
                        ((TarEntrySupplicant)entryQueue[i]).close();
                    }

                    archive.close();
                }
                catch (IOException )
                {

                    // Too difficult to report every single error.
                    // More important that the user know about the original Exc.
                }

                throw ioe;
            }
        }

        /**
         * Slots for supplicant files and input streams to be added to a Tar
         * archive.
         *
         * @author Blaine Simpson (blaine dot simpson at admc dot com)
         */
        protected class TarEntrySupplicant
        {

            static protected byte[] HEADER_TEMPLATE =
                (byte[])TarFileOutputStream.ZERO_BLOCK.Clone();
            //static Char swapOutDelim = (Char)0;
            protected static byte[] ustarBytes = {
           (byte) 'u', (byte) 's',(byte)  't', (byte) 'a',(byte)  'r'
        };

            static TarEntrySupplicant()
            {

                //swapOutDelim = '\\';


                try
                {
                    writeField(TarHeaderFields.UID, 0L, HEADER_TEMPLATE);
                    writeField(TarHeaderFields.GID, 0L, HEADER_TEMPLATE);
                }
                catch (TarMalformatException tme)
                {

                    // This would definitely get caught in Dev env.
                    throw new Exception(tme.ToString());
                }

                // Setting uid and gid to 0 = root.
                // Misleading, yes.  Anything better we can do?  No.
                int magicStart = TarHeaderFields.getStart(TarHeaderFields.MAGIC);

                for (int i = 0; i < ustarBytes.Length; i++)
                {

                    // UStar magic field
                    HEADER_TEMPLATE[magicStart + i] = ustarBytes[i];
                }

                HEADER_TEMPLATE[263] = (byte)'0';
                HEADER_TEMPLATE[264] = (byte)'0';

                // UStar version field, version = 00
                // This is the field that Gnu Tar desecrates.
            }

            static protected void writeField(int fieldId, String newValue,
                                             byte[] target)
            {

                int start = TarHeaderFields.getStart(fieldId);
                int stop = TarHeaderFields.getStop(fieldId);
                byte[] ba;

              
                ba = Encoding.ASCII.GetBytes( newValue);
               

                if (ba.Length > stop - start)
                {
                    throw new TarMalformatException(
                        RB.singleton.getString(
                            RB.TAR_FIELD_TOOBIG,
                            TarHeaderFields.toString(fieldId), newValue));
                }

                for (int i = 0; i < ba.Length; i++)
                {
                    target[start + i] = ba[i];
                }
            }

            static protected void clearField(int fieldId, byte[] target)
            {

                int start = TarHeaderFields.getStart(fieldId);
                int stop = TarHeaderFields.getStop(fieldId);

                for (int i = start; i < stop; i++)
                {
                    target[i] = 0;
                }
            }

            static protected void writeField(int fieldId, long newValue,
                                             byte[] target)
            {

                writeField(
                    fieldId,
                    prePaddedOctalString(
                        newValue,
                        TarHeaderFields.getStop(fieldId)
                        - TarHeaderFields.getStart(fieldId)), target);
            }

            static public String prePaddedOctalString(long val, int width)
            {
                return StringUtil.toPaddedString(String.Format("{0:o}", val), width,
                                                 '0', false);
            }

            protected byte[] rawHeader = (byte[])HEADER_TEMPLATE.Clone();
            protected String fileMode = DEFAULT_FILE_MODES;

            // Following fields are always initialized by constructors.
            protected Stream inputStream;
            protected String path;
            protected long modTime;
            protected TarFileOutputStream tarStream;
            protected long dataSize;    // In bytes
            protected bool paxSized = false;

            // Size will only be written to entry's header if paxSized is false.
            public String getPath()
            {
                return path;
            }

            public long getDataSize()
            {
                return dataSize;
            }

            /*
             * Internal constructor that validates the entry's path.
             */
            protected TarEntrySupplicant(String path, char typeFlag,
                                         TarFileOutputStream tarStream)
            {

                if (path == null)
                {
                    throw new ArgumentException(
                        RB.singleton.getString(RB.MISSING_SUPP_PATH));
                }

                this.path = path;
                                                  
                this.tarStream = tarStream;

                writeField(TarHeaderFields.TYPEFLAG, typeFlag);

                if (typeFlag == '\0' || typeFlag == ' ')
                {
                    writeField(TarHeaderFields.UNAME,
                               "EffiProz", HEADER_TEMPLATE);
                    writeField(TarHeaderFields.GNAME, "root", HEADER_TEMPLATE);

                    // Setting UNAME and GNAME at the instance level instead of the
                    // static template, because record types 'x' and 'g' do not set
                    // these fields.
                    // POSIX UStar compliance requires that we set "gname" field.
                    // It's impossible for use to determine the correct value from
                    // Java.  We punt with "root" because (a) it's the only group
                    // name
                    // we know should exist on every UNIX system, and (b) every tar
                    // client gracefully handles it when extractor user does not
                    // have privs for the specified group.
                }
            }

            /**
             * This creates a 'x' entry for a 0/\0 entry.
             */
            public TarEntrySupplicant makeXentry()
            {

                PIFGenerator pif = new PIFGenerator(new FileInfo(path));

                pif.addRecord("size", dataSize);

                paxSized = true;

                // This tells the target entry to NOT set the size header field,
                // to ensure that no tar client accidentally extracts only
                // a portion of the file data.
                // If the client can't read the correct size from the PIF data,
                // we want the client to report that so the user can get a better
                // tar client!
                /* Really bad to make pseudo-stream just to get a byte array out
                 * of it, but it would be a very poor use of development time to
                 * re-design this class because the comparative time wasted at
                 * runtime will be negligable compared to storing the data entries.
                 */
                return new TarEntrySupplicant(
                    pif.getName(), (pif),
                   (int)pif.Length, 'x', tarStream);
            }

            /**
             * After instantiating a TarEntrySupplicant, the user must either invoke
             * write() or close(), to release system resources on the input
             * File/Stream.
             */
            public TarEntrySupplicant(String path, FileInfo file,
                                      TarFileOutputStream tarStream)
                : this(((path == null) ? file.DirectoryName
                    : path), '0', tarStream)
            {

                // Must use an expression-embedded ternary here to satisfy compiler
                // that this() call be first statement in constructor.


                // Difficult call for '0'.  binary 0 and character '0' both mean
                // regular file.  Binary 0 pre-UStar is probably more portable,
                // but we are writing a valid UStar header, and I doubt anybody's
                // tar implementation would choke on this since there is no
                // outcry of UStar archives failing to work with older tars.


                modTime = file.LastWriteTime.Ticks;
                dataSize = file.Length;
                inputStream = file.OpenWrite();
            }

            /**
             * After instantiating a TarEntrySupplicant, the user must either invoke
             * write() or close(), to release system resources on the input
             * File/Stream.
             * <P/>
             * <B>WARNING:</B>
             * Do not use this method unless the quantity of available RAM is
             * sufficient to accommodate the specified maxBytes all at one time.
             * This constructor loads all input from the specified InputStream into
             * RAM before anything is written to disk.
             *
             * @param maxBytes This method will fail if more than maxBytes bytes
             *                 are supplied on the specified InputStream.
             *                 As the type of this parameter enforces, the max
             *                 size you can request is 2GB.
             */
            public TarEntrySupplicant(String path, Stream origStream,
                                      int maxBytes, char typeFlag,
                                      TarFileOutputStream tarStream)
                : this(path, typeFlag, tarStream)
            {

                /*
                 * If you modify this, make sure to not intermix reading/writing of
                 * the PipedInputStream and the PipedOutputStream, or you could
                 * cause dead-lock.  Everything is safe if you close the
                 * PipedOutputStream before reading the PipedInputStream.
                 */


                if (maxBytes < 1)
                {
                    throw new ArgumentException(
                        RB.singleton.getString(RB.READ_LT_1));
                }

                int i;


                inputStream = new MemoryStream();

                /* This constructor not available until Java 1.6:
                inputStream = new PipedInputStream(outPipe, maxBytes);
                */
                try
                {
                    while ((i =
                            origStream
                                .Read(tarStream.writeBuffer, 0, tarStream
                                    .writeBuffer.Length)) > 0)
                    {
                        inputStream.Write(tarStream.writeBuffer, 0, i);
                    }

                    inputStream.Flush();    // Do any good on a pipe?

                    dataSize = inputStream.Length - inputStream.Position;

                    if (TarFileOutputStream.debug)
                    {
                        Console.Out.WriteLine(
                            RB.singleton.getString(
                                RB.STREAM_BUFFER_REPORT, dataSize.ToString()));
                    }
                }
                catch (IOException ioe)
                {
                    inputStream.Close();

                    throw ioe;
                }

                modTime = DateTime.Now.Ticks;
            }

            public void close()
            {
                inputStream.Close();
            }

            protected long headerChecksum()
            {

                long sum = 0;

                for (int i = 0; i < rawHeader.Length; i++)
                {
                    bool isInRange =
                        (i >= TarHeaderFields.getStart(TarHeaderFields.CHECKSUM)
                         && i < TarHeaderFields.getStop(TarHeaderFields.CHECKSUM));

                    sum += isInRange ? 32
                                     : (255 & rawHeader[i]);

                    // We ignore current contents of the checksum field so that
                    // this method will continue to work right, even if we later
                    // recycle the header or RE-calculate a header.
                }

                return sum;
            }

            protected void clearField(int fieldId)
            {
                TarEntrySupplicant.clearField(fieldId, rawHeader);
            }

            protected void writeField(int fieldId,
                                      String newValue)
            {
                TarEntrySupplicant.writeField(fieldId, newValue, rawHeader);
            }

            protected void writeField(int fieldId,
                                      long newValue)
            {
                TarEntrySupplicant.writeField(fieldId, newValue, rawHeader);
            }

            protected void writeField(int fieldId,
                                      char c)
            {
                TarEntrySupplicant.writeField(fieldId, c.ToString(),
                                              rawHeader);
            }

            /**
             * Writes entire entry to this object's tarStream.
             *
             * This method is guaranteed to close the supplicant's input stream.
             */
            public void write()
            {

                int i;

                try
                {
                    writeField(TarHeaderFields.NAME, path);

                    // TODO:  If path.Length > 99, then write a PIF entry with
                    // the file path.
                    // Don't waste time using the PREFIX header field.
                    writeField(TarHeaderFields.MODE, fileMode);

                    if (!paxSized)
                    {
                        writeField(TarHeaderFields.SIZE, dataSize);
                    }

                    writeField(TarHeaderFields.MTIME, modTime);
                    writeField(
                        TarHeaderFields.CHECKSUM,
                        TarEntrySupplicant.prePaddedOctalString(
                            headerChecksum(), 6) + "\0 ");

                    // Silly, but that's what the base header spec calls for.
                    tarStream.writeBlock(rawHeader);

                    long dataStart = tarStream.getBytesWritten();

                    while ((i = inputStream.Read(tarStream.writeBuffer, 0, tarStream.writeBuffer.Length)) > 0)
                    {
                        tarStream.write(i);
                    }

                    if (dataStart + dataSize != tarStream.getBytesWritten())
                    {
                        throw new IOException(
                            RB.singleton.getString(
                                RB.DATA_CHANGED, dataSize.ToString(),
                                (
                                    (tarStream.getBytesWritten() - dataStart)).ToString()));
                    }

                    tarStream.padCurrentBlock();
                }
                finally
                {
                    close();
                }
            }



            public const String DEFAULT_FILE_MODES = "600";

            // Be conservative, because these files contain passwords
        }
    }

}
